#!/usr/bin/env python
# -*- coding: utf-8 -*-

# (c) 2021 <Evgeniy> LGPL
# Modified by <hasecilu> on 2024-11-14

"""
Description:
This macro generates lists of properties of the selected object in different forms
in Wiki format. The list is printed to the Report view of FreeCAD. This macro can
be used to simplify working with this Wiki with some restrictions. The property
lists published in Wiki usually contain only unique parameters. Parameters inherited
from parent objects are not published.

This script automatically removes inherited parameters from the list. But due to the
software limitations of FreeCAD, it cannot delete absolutely all inherited parameters,
so the lists generated by this script should be carefully checked before publishing.

Usage:
- Preparing:
    - Copy code of Macro from this page and place it in a new *.FCMacro file.
    - Save FCMacro file, and place it in the FreeCAD Macros folder.
    - Disable Include a timecode for each entry checkbox in Preferences -> General -> Output window (Tab).

- First using:
    - Select (or create and select) only one object in tree view the type of you need.
    - Select Macro -> Macros... item in main menu.
    - In opened window find *.FCMacro file what you save letter and press Execute Button.
    - In the dialog that opens, select the type of report that you want to get by pressing button:
        - Only Main - prints only unique properties of base (parent) classes.
        - All Properties - prints absolutely all properties of selected object.
        - Inherits+Unique Properties - prints only unique properties with included inherits report.
        - Only Unique Properties - prints only unique properties.

- How to use again:
    - To quickly run this macro again, you can use keyboard shortcut `Shift+Ctrl+1` it will launch the last run macro.
"""

__Name__ = "Wiki Object Properties List Generator"
__Comment__ = "Generates lists of object properties for use in the FreeCAD Wiki documentation."
__Author__ = "Evgeniy"
__Date__ = "2024-11-23"
__Version__ = "0.16.1"
__License__ = "LGPL-3.0-or-later"  # 6 matches for LGPL on https://spdx.org/licenses/
__Web__ = ""
__Wiki__ = "https://wiki.freecad.org/Macro_Wiki_Object_Properties_List_Generator"
__Icon__ = "./WikiObjectPropertiesListGenerator.svg"
__Xpm__ = ""
__Help__ = "Select a scripted objects on the tree and execute the macro."
__Status__ = "Beta"
__Requires__ = ""
__Communication__ = "https://forum.freecad.org/viewtopic.php?f=21&t=61998"
__Files__ = "./WikiObjectPropertiesListGenerator.svg"

# NOTE: Changelog
# ---------
# - 0.16.1 - 2024-11-23
#   - Attend change requests: imports, use semver, f-string, simplify condition, function style.
#   - Add checkbox to let user decide if wants to clear or not the Report View
# - 0.16.0 - 2024-11-15
#   - Refactor code: add custom dataclass, update functions' signatures, make some fixes.
#   - Include property's value on the list.
#   - Create custom dialog to add checkbox to let user disable/enable property status.
# - 0.15.0 - 2021-09
#   - Initial release.

import re
from dataclasses import dataclass

import FreeCAD as app
import FreeCADGui as gui
from PySide import QtCore, QtGui, QtWidgets  # FreeCAD's PySide!


@dataclass
class PropertyInfo:
    property_type: str
    data_type: str
    default_value: str
    tooltip: str
    status: str


# Based on https://github.com/FreeCAD/FreeCAD/blob/5d49bf78de785a536f941f1a6d06d432582a95d3/src/App/PropertyFile.h
# and https://github.com/FreeCAD/FreeCAD/blob/5d49bf78de785a536f941f1a6d06d432582a95d3/src/App/PropertyContainerPyImp.cpp
status_dictionary = {
    0: "Touched",  # touched property
    # 1:"Immutable",   # can't modify property
    # 2:"ReadOnly",    # for property editor
    # 3:"Hidden",      # for property editor
    # 4:"Transient",   # for property container save
    # 5:"MaterialEdit", # to turn ON PropertyMaterial edit
    # 6:"NoMaterialListEdit", # to turn OFF PropertyMaterialList edit
    # 7:"Output",      # same effect as Prop_Output
    # 8:"LockDynamic", # prevent being removed from dynamic property
    # 9:"NoModify",    # prevent causing Gui::Document::setModified()
    # 10:"PartialTrigger", # allow change in partial doc
    11: "NoRecompute",  # touch owner for recompute on property change
    12: "Single",  # for save/load of floating point numbers
    13: "Ordered",  # for PropertyLists whether the order of the elements is relevant for the container using it
    14: "EvalOnRestore",  # In case of expression binding, evaluate the expression on restore and touch the object on value change.
    # The following bits are corresponding to PropertyType set when the
    # property added. These types are meant to be static, and cannot be
    # changed in runtime. It is mirrored here to save the linear search
    # required in PropertyContainer::getPropertyType()
    21: "Dynamic",  # indicating the property is dynamically added
    22: "NoPersist",  # corresponding to Prop_NoPersist
    23: "NoRecompute",  # corresponding to Prop_NoRecompute
    24: "ReadOnly",  # corresponding to Prop_ReadOnly
    25: "Transient",  # corresponding to Prop_Transient
    26: "Hidden",  # corresponding to Prop_Hidden
    27: "Output",  # corresponding to Prop_Output
}


def clear_report_view():
    """Clear content of Report View panel."""
    main_window = gui.getMainWindow()
    main_window.findChild(QtGui.QTextEdit, "Report view").clear()


def print_sr(text: str, show_report: bool = True):
    """Print the given text if "show report" flag is enabled."""
    app.Console.PrintMessage(text) if show_report else None


def generate_markup_list(grouped_properties: dict, print_status: bool = False):
    """Print to the console the Wiki markup code."""
    # NOTE: Group example:
    # {{Properties_Title|Box features}}
    # Property example:
    # * {{PropertyData|BoxOpenFront|Bool}}: Default is {{value|False}}. Leave front of box open
    # or:
    # * {{PropertyData|BoxOpenFront|Bool|Dynamic}}: Default is {{value|False}}. Leave front of box open

    # Sort groups and properties within each group
    for group in sorted(grouped_properties):
        app.Console.PrintMessage(f"\n{{{{Properties_Title|{group}}}}}\n\n")
        for prop, data in sorted(grouped_properties[group]):
            if print_status:
                status = "|" + data.status
            elif "Hidden" in data.status:  # make exception, all other status will be ignored
                status = "|" + "hidden"  # manually use lowercase
            else:
                status = ""
            app.Console.PrintMessage(
                f"* {{{{Property{data.property_type}|{prop}|{data.data_type}{status}}}}}: "
                f"Default is {{{{value|{data.default_value}}}}}. {data.tooltip}.\n"
            )
    app.Console.PrintMessage("\n\n")


def print_obj_properties(
    obj, typ: str, ignore_list: dict, show_inherits: bool = False, print_status: bool = False
):
    """Print non-ignored properties of the selected object to the Report View."""
    grouped_properties = {}
    inherits = ""
    for prop in obj.PropertiesList:
        value = getattr(obj, prop, "N/A")
        typeid = (obj.getTypeIdOfProperty(prop) or "").replace("App::Property", "")
        prop_group = obj.getGroupOfProperty(prop) or ""
        tooltip = obj.getDocumentationOfProperty(prop) or ""
        # Get all possible status variants
        stat = obj.getPropertyStatus(prop) or ""

        if stat:
            for st in stat:
                # get status without name from dictionary
                if isinstance(st, int):
                    stat.insert(stat.index(st), status_dictionary[st])
                    stat.remove(st)
            # remove symbols ['] from str by regexp and trim spaces
            stat = re.sub(r"[\[\]\']", "", str(stat)).strip()
            # NOTE: Uncomment next two lines to make the list look like the list of parameters from the editor
            # if 'Hidden' in stat:
            #   continue
        else:
            stat = ""

        if prop not in ignore_list:
            # Add property information to the respective group
            if prop_group not in grouped_properties:
                grouped_properties[prop_group] = []
            grouped_properties[prop_group].append(
                (
                    prop,
                    PropertyInfo(
                        property_type=typ,
                        data_type=typeid,
                        default_value=value,
                        tooltip=tooltip,
                        status=stat,
                    ),
                )
            )

        else:
            inherits += f"{prop} inherited from class {ignore_list[prop]}\n"

    if inherits != "" and show_inherits:
        # angle brackets is masked as \x3C and \x3E so this code can be published in the Wiki
        print(f"\n\x3c!--\n{inherits}--\x3e")

    generate_markup_list(grouped_properties, print_status)


def add_properties_to_ignore_list(
    obj,
    ignore_list: dict,
    obj_type: str,
    show_report: bool = True,
):
    """Add properties to the ignore list if not present"""
    for prop in obj.PropertiesList:
        if prop not in ignore_list:
            ignore_list[prop] = obj_type
            atr = str(obj.getPropertyStatus(prop)).replace("[]", "")
            typeid = (obj.getTypeIdOfProperty(prop) or "").replace("App::Property", "")
            print_sr(f"{prop} ({typeid}) {atr}", show_report)
    return ignore_list


def add_to_ignore_list(
    data_ignore_list: dict,
    view_ignore_list: dict,
    obj_type: str,
    temp_obj_name: str,
    show_report: bool = True,
):
    """Add properties to ignore_list if they are not present"""
    print_sr(f"Class: '{obj_type}'", show_report)

    # Make temporary object and get its Data parameters
    obj = app.activeDocument().addObject(obj_type, temp_obj_name)
    name = obj.Name
    print_sr("Data parameters list:", show_report)
    data_ignore_list = add_properties_to_ignore_list(obj, data_ignore_list, obj_type, show_report)

    # Try to get View parameters if possible
    view_obj = obj.ViewObject

    try:
        print_sr("View parameters list:", show_report)
        view_ignore_list = add_properties_to_ignore_list(
            view_obj, view_ignore_list, obj_type, show_report
        )
    except:
        print_sr("This class does not support View parameters.", show_report)

    # Remove temporary object
    app.activeDocument().removeObject(name)
    print_sr("", show_report)

    return data_ignore_list, view_ignore_list


def fill_ignore_lists(show_report: bool = True):
    data_ignore_list = {}
    view_ignore_list = {}

    # List of object types to process
    object_types = [
        # "App::OriginGroupExtension", # Interfaces are not supported
        # "Part::AttachExtension",
        # "Sketcher::PropertyConstrainList",
        "App::DocumentObject",
        "App::GeoFeature",
        "App::Part",
        "Mesh::Feature",
        "Fem::FemMeshObject",
        "Path::Feature",
        "Part::Part2DObject",
        # "Part::Primitive",  # Part::Primitive is not possible to create
        "PartDesign::Feature",
        "Part::BodyBase",
        "PartDesign::Body",
    ]

    # Add properties for each object type
    for obj_type in object_types:
        data_ignore_list, view_ignore_list = add_to_ignore_list(
            data_ignore_list, view_ignore_list, obj_type, "Temp", show_report
        )

    print_sr(
        f"List of all not unique properties of Data:\n{str(data_ignore_list)}\n\n"
        f"List of all not unique properties of View:\n{str(view_ignore_list)}\n",
        show_report,
    )

    return data_ignore_list, view_ignore_list


class CustomDialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setWindowTitle("Properties Generator")
        self.resize(300, 150)
        self.button_strings = [
            "Only Main",
            "All Properties",
            "Inherits + Unique Properties",
            "Only Unique Properties",
        ]

        # Layouts
        layout = QtWidgets.QVBoxLayout(self)
        button_layout = QtWidgets.QHBoxLayout()

        # Instruction Label
        self.instruction_label = QtWidgets.QLabel()
        self.instruction_label.setTextFormat(QtCore.Qt.RichText)
        self.instruction_label.setText(
            "Select the type of report you need:<br><br>"
            "<b>Only Main</b> -> print only unique properties of main classes with types.<br>"
            "<b>All Properties</b> -> print all properties of selected object.<br>"
            "<b>Inherits + Unique Properties</b> -> print unique properties with inherits report.<br>"
            "<b>Only Unique Properties</b> -> print only unique properties of selected object.<br>",
        )
        layout.addWidget(self.instruction_label)

        # Checkboxes
        self.include_prop_status = QtWidgets.QCheckBox("Include status of the property", self)
        layout.addWidget(self.include_prop_status)
        self.clear_report_view = QtWidgets.QCheckBox("Clear all content on Report View", self)
        layout.addWidget(self.clear_report_view)

        # Buttons
        button_om = QtWidgets.QPushButton(self.button_strings[0], self)
        button_om.clicked.connect(self.handle_button_click)
        button_layout.addWidget(button_om)
        button_ap = QtWidgets.QPushButton(self.button_strings[1], self)
        button_ap.clicked.connect(self.handle_button_click)
        button_layout.addWidget(button_ap)
        button_iup = QtWidgets.QPushButton(self.button_strings[2], self)
        button_iup.clicked.connect(self.handle_button_click)
        button_layout.addWidget(button_iup)
        button_oup = QtWidgets.QPushButton(self.button_strings[3], self)
        button_oup.clicked.connect(self.handle_button_click)
        button_layout.addWidget(button_oup)

        layout.addLayout(button_layout)

        # Cancel Button
        self.cancel_button = QtWidgets.QPushButton("Cancel", self)
        self.cancel_button.clicked.connect(self.reject)
        layout.addWidget(self.cancel_button)

        # State tracking
        self.pressed_button = None

    def handle_button_click(self):
        sender = self.sender()
        if sender:
            self.pressed_button = sender.text()
            self.accept()


# Function to display dialog and retrieve data
def show_dialog():
    dialog = CustomDialog(gui.getMainWindow())
    result = dialog.exec()

    if result == QtWidgets.QDialog.Accepted:
        if dialog.clear_report_view.isChecked():
            clear_report_view()
        if dialog.pressed_button == dialog.button_strings[0]:
            data_ignore_list, view_ignore_list = fill_ignore_lists(True)
        elif dialog.pressed_button in dialog.button_strings[1:4]:  # 1,2,3
            name = sel[0].Name
            act_obj = gui.activeDocument().getObject(name)
            data_ignore_list = {}
            view_ignore_list = {}
            if dialog.pressed_button in dialog.button_strings[2:4]:  # 2,3
                data_ignore_list, view_ignore_list = fill_ignore_lists(False)
            print("\n== Properties ==")
            print("\n=== View ===")
            print_obj_properties(
                act_obj,
                "View",
                view_ignore_list,
                dialog.pressed_button == dialog.button_strings[3],
                dialog.include_prop_status.isChecked(),
            )
            print("\n=== Data ===")
            act_obj = app.activeDocument().getObject(name)
            print_obj_properties(
                act_obj,
                "Data",
                data_ignore_list,
                dialog.pressed_button == dialog.button_strings[3],
                dialog.include_prop_status.isChecked(),
            )
    else:
        print("Dialog canceled")


####################################################################################################
# Main code

sel = gui.Selection.getSelection()
if len(sel) != 1:
    msgbox = QtGui.QMessageBox(
        QtGui.QMessageBox.Question,
        "Properties Generator",
        "You must select exactly one object in tree view!",
    )
    msgbox.exec()
else:
    show_dialog()
